/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ts, Local, Type, ArkMethod, AbstractBinopExpr, LocalSignature, ArkAssignStmt, MethodSignature, Cfg, FullPosition, LineColPosition, ArkNormalBinopExpr, LiteralType, AstTreeUtils, ArkConditionExpr } from "arkanalyzer/lib";
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import { Defects, IssueReport } from "../../model/Defects";
import { ClassMatcher, FileMatcher, MatcherCallback, MatcherTypes, MethodMatcher } from "../../matcher/Matchers";
import { Rule } from "../../model/Rule";
import { RuleListUtil } from "../../utils/common/DefectsList";
type localType = {
    name: string,
    originalType: {
        currType?: LiteralType[],
        types?: LiteralType[],
        name?: string
    },
    signature: LocalSignature
};
type LType = {
    literalName: string,
    name: string
};
type classSignature = {
    classSignature: {
        className: string,
        declaringFilesSignature: {
            fileName: string,
            hashcode: number,
            projectName: string
        },
        declaringNamespaceSignature: {}
    },
    realGenericTypes: {}
};
type RightOp = {
    args: [],
    base: {
        declaringStmt: ArkAssignStmt,
        name: string,
        originalValue: string,
        type: Type,
        usedStmts: ArkAssignStmt[]
    },
    methodSignature: MethodSignature,
    realGenericTypes: Type
};
type RightOp1 = {
    declaringStmt: ArkAssignStmt,
    name: string,
    originalValue: string,
    type: Type,
    usedStmts: ArkAssignStmt[]
};
type LeftOp = {
    declaringStmt: ArkAssignStmt,
    name: string,
    originalValue: string,
    type: Type,
    usedStmts: ArkAssignStmt[]
};
type usedStmt = {
    cfg: Cfg,
    leftOp: Local,
    operandOriginalPositions: FullPosition[],
    originalPosition: LineColPosition,
    originalText: string,
    rightOp: ArkNormalBinopExpr
};
type UsedStmt = {
    cfg: Cfg,
    conditionExpr: ArkConditionExpr,
    operandOriginalPositions: FullPosition[],
    originalPosition: LineColPosition
};
const defaultOptions: Options = {
    allowDefaultCaseForExhaustiveSwitch: true,
    requireDefaultForNonUnion: false
};
interface Options {
    allowDefaultCaseForExhaustiveSwitch: boolean,
    requireDefaultForNonUnion: boolean
};
interface lineColumnInfo {
    line: number,
    startCol: number,
    endCol: number,
    filePath: string
};
const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'SwitchExhaustivenessCheck');
const gmetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: "docs/switch-exhaustiveness-check.md", // TODO: support url
    description: "Require switch-case statements to be exhaustive with union type."
};
export class SwitchExhaustivenessCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gmetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
    };
    private classMatcher: ClassMatcher = {
        file: [this.fileMatcher],
        matcherType: MatcherTypes.CLASS
    };
    private methodMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD,
        class: [this.classMatcher]
    };
    public registerMatchers(): MatcherCallback[] {
        const matchBuildCb: MatcherCallback = {
            matcher: this.methodMatcher,
            callback: this.check
        }
        return [matchBuildCb];
    }
    private static readonly sourceFileCache = new Map<string, ts.SourceFile>();
    public check = (method: ArkMethod) => {
        const options = this.getCheckerOptions();
        const {
            typesSet,
            classTypeSet,
            typeNameSet,
            variableNameSet,
            trueSwitch,
            arrayLength
        } = this.initCheckerDataStructures();
        this.processClassStaticFields(method, classTypeSet, typeNameSet);
        if (method instanceof ArkMethod) {
            // 提取本地变量处理逻辑
            this.processLocalVariables(method, options, typesSet, classTypeSet,
                typeNameSet, variableNameSet, trueSwitch, arrayLength);
        }
    }
    private getCheckerOptions(): Options {
        return this.rule?.option?.length > 0 ?
            this.rule.option[0] as Options :
            defaultOptions;
    }
    private initCheckerDataStructures(): {
        typesSet: Set<string>,
        classTypeSet: Set<string>,
        typeNameSet: Set<string>,
        variableNameSet: Set<string>,
        trueSwitch: Set<string>,
        arrayLength: Set<string>
    } {
        return {
            typesSet: new Set<string>(),
            classTypeSet: new Set<string>(),
            typeNameSet: new Set<string>(),
            variableNameSet: new Set<string>(),
            trueSwitch: new Set<string>(),
            arrayLength: new Set<string>()
        };
    }
    private processClassStaticFields(method: ArkMethod, classTypeSet: Set<string>, typeNameSet: Set<string>): void {
        const declareClass = method.getDeclaringArkClass();
        const arkFile = declareClass.getDeclaringArkFile();
        const classes = arkFile.getClasses();

        for (const Class of classes) {
            if (!Class) { continue };

            const classSignature = Class.getSignature();
            const staticFields = Class.getStaticFields(new Map().set(arkFile.getFileSignature(), classes));

            if (staticFields?.length) {
                typeNameSet.add(classSignature?.getClassName());
                this.updateClassTypeSet(staticFields, classTypeSet);
            }
        }
    }
    private updateClassTypeSet(staticFields: any[], classTypeSet: Set<string>): void {
        for (const staticFiled of staticFields) {
            const fieldName = staticFiled.getSignature().getFieldName();
            if (classTypeSet.size > 2) { classTypeSet.clear() };
            if (!classTypeSet.has(fieldName)) {
                classTypeSet.add(fieldName);
            }
        }
    }
    private processLocalVariables(method: ArkMethod, options: Options,
        typesSet: Set<string>, classTypeSet: Set<string>, typeNameSet: Set<string>,
        variableNameSet: Set<string>, trueSwitch: Set<string>, arrayLength: Set<string>): void {

        const defaultArkMethod = method.getDeclaringArkClass()?.getDefaultArkMethod();
        const locals = defaultArkMethod?.getBody()?.getLocals() ?? [];

        for (const localValue of locals.values() || []) {
            const localType = localValue?.getType() as unknown as localType;
            if (this.shouldSkipType(localType)) { continue };

            this.processLocalType(localType, typesSet, typeNameSet);
            this.processStatements(method, localValue, options,
                classTypeSet, typeNameSet, variableNameSet, trueSwitch, arrayLength, typesSet);
        }
    }
    private processLocalType(
        localType: localType,
        typesSet: Set<string>,
        typeNameSet: Set<string>
    ): void {
        const typeName = localType?.name;
        if (typeName) {
            typeNameSet.add(typeName);
            const originalType = localType?.originalType;
            if (!originalType) { return };

            // 处理原始类型中的类型定义
            const types = originalType.types ?? [];
            this.processLocalType1(types, typesSet);
        }
    }
    private processLocalType1(types: LiteralType[], typesSet: Set<string>): void {
        if (!types) { return };

        for (const Type of types) {
            const type = Type as unknown as LType;
            const literalName = type?.literalName;
            if (!typesSet.has(literalName)) {
                typesSet.add(literalName);
            }
        }
    }
    private shouldSkipType(localType: localType): boolean {
        const skipTypes = new Set(['string', 'number', 'boolean', 'undefined', 'null', 'void']);
        return skipTypes.has(localType.name);
    }
    private processStatements(method: ArkMethod, localValue: any, options: Options,
        classTypeSet: Set<string>, typeNameSet: Set<string>, variableNameSet: Set<string>,
        trueSwitch: Set<string>, arrayLength: Set<string>, typesSet: Set<string>): void {

        const stmts = method.getBody()?.getCfg().getStmts() ?? [];
        for (const stmt of stmts) {
            if (!(stmt instanceof ArkAssignStmt)) { continue };

            this.processLeftOperand(stmt, typeNameSet, variableNameSet);
            this.processRightOperand(stmt, arrayLength);
            this.processUsedStatements(localValue, options,
                classTypeSet, variableNameSet, trueSwitch, typesSet, method);
        }
    }
    private processLeftOperand(stmt: ArkAssignStmt, typeNameSet: Set<string>, variableNameSet: Set<string>): void {
        const leftOp = stmt.getLeftOp();
        const leftOpType = leftOp?.getType() as unknown as localType;
        const leftOpTypeName = leftOpType?.name;

        // 处理枚举类型变量名
        const classType = leftOp?.getType() as unknown as classSignature;
        const className = classType?.classSignature?.className;
        if (className && typeNameSet.has(className)) {
            variableNameSet.add(leftOp.toString());
        }

        // 处理联合类型变量名
        if (leftOpTypeName && typeNameSet.has(leftOpTypeName)) {
            variableNameSet.add(leftOp.toString());
        }
    }
    private processRightOperand(stmt: ArkAssignStmt, arrayLength: Set<string>): void {
        const rightOp = stmt.getRightOp() as unknown as RightOp;
        const rightOpBase = rightOp?.base;

        if (rightOpBase?.name === 'Math') {
            const leftOpUsedStmts = (stmt.getLeftOp() as unknown as LeftOp)?.usedStmts;
            for (const usedStmt of leftOpUsedStmts || []) {
                this.processRightOperand1(usedStmt, arrayLength);
            }
        }
    }
    private processRightOperand1(usedStmt: ArkAssignStmt, arrayLength: Set<string>): void {
        const binopExpr = (usedStmt as unknown as usedStmt).rightOp;
        if (binopExpr instanceof AbstractBinopExpr) {
            const op1 = binopExpr.getOp1() as unknown as RightOp1;
            const op2 = binopExpr.getOp2() as unknown as RightOp1;
            // 提取数组长度相关值
            this.processRightOperand11(op1, op2, arrayLength);
        }
    }
    private processRightOperand11(op1: RightOp1, op2: RightOp1, arrayLength: Set<string>): void {
        if (op1?.type?.toString() === 'unknown' && op2?.type) {
            arrayLength.add(op2.toString());
        }
        if (op2?.type?.toString() === 'unknown' && op1?.type) {
            arrayLength.add(op1.toString());
        }
    }
    private processUsedStatements(localValue: any, options: Options,
        classTypeSet: Set<string>, variableNameSet: Set<string>,
        trueSwitch: Set<string>, typesSet: Set<string>, method: ArkMethod): void {

        const usedStmts = localValue?.getUsedStmts() || [];

        this.processUsedStatements1(usedStmts, options,
            classTypeSet, variableNameSet, trueSwitch, typesSet, method);
    }
    private processUsedStatements1(usedStmts: any, options: Options,
        classTypeSet: Set<string>, variableNameSet: Set<string>,
        trueSwitch: Set<string>, typesSet: Set<string>, method: ArkMethod): void {
        for (const usedStmt of usedStmts) {
            const expr = (usedStmt as unknown as UsedStmt).conditionExpr;
            if (!expr || expr.getType()?.getTypeString() !== 'boolean') { continue };
            this.validateSwitchExhaustiveness(expr, usedStmts, options,
                classTypeSet, variableNameSet, trueSwitch, typesSet, method);
        }
    }
    private validateSwitchExhaustiveness(expr: ArkConditionExpr, usedStmts: ArkAssignStmt[], options: Options,
        classTypeSet: Set<string>, variableNameSet: Set<string>,
        trueSwitch: Set<string>, typesSet: Set<string>, method: ArkMethod): void {
        const typeString = expr?.getType()?.getTypeString();
        let declareClass = method.getDeclaringArkClass();
        const arkFile = declareClass.getDeclaringArkFile();
        const code = arkFile.getCode() ?? '';
        const arkFilePath = arkFile.getFilePath();
        if (typeString !== 'boolean') { return };
        const exprOp = expr?.getUses() ?? [];
        const [exprOp1, exprOp2] = exprOp;
        const exprOp1String = exprOp1?.toString() ?? '';
        const exprOp2String = exprOp2?.toString() ?? '';
        const switchHasDefault = this.checkSwitchHasDefaultCase(code, exprOp1String);
        this.validateSwitchExhaustiveness1(exprOp1String, exprOp2String, usedStmts, options,
            classTypeSet, variableNameSet, trueSwitch, typesSet, switchHasDefault, code, arkFilePath);
        this.otherCheck(usedStmts, classTypeSet, switchHasDefault, exprOp1String, code, arkFilePath, trueSwitch, options);
    }
    private validateSwitchExhaustiveness1(exprOp1String: string, exprOp2String: string, usedStmts: ArkAssignStmt[], options: Options,
        classTypeSet: Set<string>, variableNameSet: Set<string>, trueSwitch: Set<string>, typesSet: Set<string>,
        switchHasDefault: boolean, code: string, arkFilePath: string): void {
        if (variableNameSet.has(exprOp1String)) {
            //type联合类型判断
            if (typesSet.has(exprOp2String)) {
                this.typesSetHasExprOp2String(usedStmts, typesSet, switchHasDefault, exprOp1String, code, arkFilePath);
            }
            if (variableNameSet.has(exprOp2String)) {
                this.variableNameSetHasExprOp2String(usedStmts, classTypeSet, switchHasDefault, exprOp1String, trueSwitch, options);
                //enum枚举类型判断
                this.enumCheck(usedStmts, classTypeSet, switchHasDefault, exprOp1String, code, arkFilePath, trueSwitch);
            }
        }
    }
    private typesSetHasExprOp2String(usedStmts: ArkAssignStmt[], typesSet: Set<string>,
        switchHasDefault: boolean, exprOp1String: string, code: string, arkFilePath: string): void {
        if (usedStmts.length !== typesSet.size && !switchHasDefault) {
            this.addIssueReport(code, arkFilePath, exprOp1String);
        }
    }
    private variableNameSetHasExprOp2String(usedStmts: ArkAssignStmt[], classTypeSet: Set<string>,
        switchHasDefault: boolean, exprOp1String: string, trueSwitch: Set<string>, options: Options): void {
        if (usedStmts.length === classTypeSet.size || switchHasDefault) {
            if (!trueSwitch.has(exprOp1String)) {
                trueSwitch.add(exprOp1String);
            }
        }
        this.variableNameSetHasExprOp2String1(usedStmts, classTypeSet, switchHasDefault, exprOp1String, trueSwitch, options);
    }
    private variableNameSetHasExprOp2String1(usedStmts: ArkAssignStmt[], classTypeSet: Set<string>,
        switchHasDefault: boolean, exprOp1String: string, trueSwitch: Set<string>, options: Options): void {
        if (options.allowDefaultCaseForExhaustiveSwitch === false) {
            if (usedStmts.length < classTypeSet.size && switchHasDefault) {
                if (!trueSwitch.has(exprOp1String)) {
                    trueSwitch.add(exprOp1String);
                }
            }
        }
    }
    private enumCheck(usedStmts: ArkAssignStmt[], classTypeSet: Set<string>,
        switchHasDefault: boolean, exprOp1String: string, code: string, arkFilePath: string,
        trueSwitch: Set<string>): void {
        if (usedStmts.length !== classTypeSet.size && !switchHasDefault) {
            //排除特殊情况
            if (usedStmts.length === 1) {
                this.enumCheck1(usedStmts, code, arkFilePath, trueSwitch);
            } else {
                this.addIssueReport(code, arkFilePath, exprOp1String);
            }
        }
    }
    private enumCheck1(usedStmts: ArkAssignStmt[], code: string, arkFilePath: string, trueSwitch: Set<string>): void {
        for (const UsedStmt of usedStmts) {
            let exprs = UsedStmt?.getExprs() ?? [];
            for (const Expr of exprs) {
                let ExprOp = Expr?.getUses();
                let ExprOp1 = ExprOp[0] ?? '';
                let ExprOp1String = ExprOp1?.toString();
                if (trueSwitch.has(ExprOp1String)) {
                    continue;
                }
                this.addIssueReport(code, arkFilePath, ExprOp1String);
            }
        }
    }
    private otherCheck(usedStmts: ArkAssignStmt[], classTypeSet: Set<string>,
        switchHasDefault: boolean, exprOp1String: string, code: string, arkFilePath: string,
        trueSwitch: Set<string>, options: Options): void {
        if (options.allowDefaultCaseForExhaustiveSwitch === false) {
            if (usedStmts.length === classTypeSet.size && switchHasDefault) {
                //排除特殊情况
                this.otherCheck1(usedStmts, trueSwitch, code, arkFilePath, exprOp1String);
            }
        }
    }
    private otherCheck1(usedStmts: ArkAssignStmt[], trueSwitch: Set<string>, code: string,
        arkFilePath: string, exprOp1String: string): void {
        if (usedStmts.length === 1) {
            for (const UsedStmt of usedStmts) {
                this.otherCheck11(UsedStmt, trueSwitch, code, arkFilePath);
            }
        } else {
            this.addIssueReport(code, arkFilePath, exprOp1String);
        }
    }
    private otherCheck11(usedStmts: ArkAssignStmt, trueSwitch: Set<string>, code: string,
        arkFilePath: string): void {
        let exprs = usedStmts?.getExprs() ?? [];
        for (const Expr of exprs) {
            let ExprOp = Expr?.getUses();
            let ExprOp1 = ExprOp[0] ?? '';
            let ExprOp1String = ExprOp1?.toString();
            if (trueSwitch.has(ExprOp1String)) {
                continue;
            }
            this.addIssueReport(code, arkFilePath, ExprOp1String);
        }
    }
    private getSourceFile(code: string, fileName: string): ts.SourceFile {
        // 检查静态缓存
        let sourceFile = SwitchExhaustivenessCheck.sourceFileCache.get(code);
        if (!sourceFile) {
            sourceFile = AstTreeUtils.getASTNode(fileName, code);
            SwitchExhaustivenessCheck.sourceFileCache.set(code, sourceFile);
        }
        return sourceFile;
    }
    /**
   * 检查 TypeScript 代码中 switch 代码块中的参数位置。
   * @param code 要检查的 TypeScript 代码
   * @returns 包含 switch 语句参数位置的对象数组
   */
    public checkSwitchParams(code: string, name: string): { line: number, character: number }[] {
        // 创建一个源代码文件对象
        const sourceFile = this.getSourceFile(code, 'temp.ts');
        // 初始化一个数组来存储 switch 语句参数的位置
        const switchParamsPositions: { line: number, character: number }[] = [];

        // 定义一个函数来检查节点
        function checkNode(node: ts.Node) {
            // 如果节点是一个 switch 语句
            if (ts.isSwitchStatement(node)) {
                // 获取 switch 语句的参数
                const switchParam = node.expression;
                if (switchParam) {
                    const paramText = switchParam.getText(sourceFile);
                    checkNode1(paramText, switchParam);
                }
            }
            // 递归检查节点的所有子节点
            ts.forEachChild(node, checkNode);
        }
        function checkNode1(paramText: string, switchParam: ts.Expression): void {
            if (paramText.includes(name)) {
                if (!sourceFile) {
                    return;
                }
                // 获取参数的起始位置
                const { line, character } = sourceFile.getLineAndCharacterOfPosition(switchParam.getStart());
                // 将参数的位置添加到数组中
                switchParamsPositions.push({ line: line + 1, character: character + 1 });
            }
        }
        // 从源代码文件的根节点开始检查
        checkNode(sourceFile);

        // 返回所有 switch 语句参数的位置
        return switchParamsPositions;
    }
    /**
     * 检查 TypeScript 代码中 switch 代码块中是否存在 default case。
     * @param code 要检查的 TypeScript 代码
     * @returns 如果存在 default case，则返回 true；否则返回 false。
     */
    public checkSwitchHasDefaultCase(code: string, name: string): boolean {
        const sourceFile = this.getSourceFile(code, 'temp.ts');
        let hasDefaultCase = false;

        function checkNode(node: ts.Node) {
            if (ts.isSwitchStatement(node)) {
                // 获取 switch 语句的参数表达式
                const switchParam = node.expression;
                if (switchParam) {
                    checkNode1(node, switchParam);
                }
                // 无论是否找到匹配的 switch 语句，都继续递归检查其他节点
                ts.forEachChild(node, checkNode);
            } else {
                // 对于非 switch 语句的节点，也继续递归检查
                ts.forEachChild(node, checkNode);
            }
        }
        function checkNode1(node: ts.SwitchStatement, switchParam: ts.Expression): void {
            // 获取参数表达式的文本内容
            const paramText = switchParam.getText(sourceFile);

            // 检查参数文本是否与传入的 name 相匹配
            if (paramText.trim() === name) {
                // 检查当前 switch 语句是否有 default case
                const cases = node.caseBlock.clauses;
                if (cases.some(clause => ts.isDefaultClause(clause))) {
                    hasDefaultCase = true;
                }
            }
        }
        checkNode(sourceFile);
        return hasDefaultCase;
    }
    private addIssueReport(code: string, arkFilePath: string, name: string): void {
        const severity = this.rule.alert ?? this.metaData.severity;
        const warnInfo = this.getLineAndColumn(code, arkFilePath, name);
        this.metaData.description = this.metaData.description;
        if (warnInfo) {
            const filePath = arkFilePath;
            let defects = new Defects(warnInfo.line, warnInfo.startCol, warnInfo.endCol, this.metaData.description, severity, this.rule.ruleId, filePath,
                this.metaData.ruleDocPath, true, false, false);
            this.issues.push(new IssueReport(defects, undefined));
            RuleListUtil.push(defects);
        }
    }
    private getLineAndColumn(code: string, arkFilePath: string, name: string): lineColumnInfo | null {
        const positions = this.checkSwitchParams(code, name);
        const validPosition = positions.
            map(position => ({
                line: position.line,
                startCol: position.character,
                endCol: position.character,
                filePath: arkFilePath
            })).find(position => position.line > 0);
        return validPosition || null;
    }
}